5편. vue 전체 예시 이해 (main.ts)
앞선 1~4편을 한 줄로 요약: 렌더는 effect다. 상태를 읽으면 track, 바꾸면 trigger, 그리고 배치로 다시 렌더. 5편에서는 main.ts를 기준으로 이 흐름을 실제 실행 순서대로 끝까지 따라가 본다. (Counter/TodoList 예제를 전제로 설명한다.)
0. 부팅: createApp(App).mount('#app')
createApp(App)이 루트 VNode를 만들고,mount('#app')에서 컨테이너 DOM을 잡는다.render(rootVNode, container)→patch(null, rootVNode, container)→ 컴포넌트 분기로mountComponent진입.mountComponent내부에서updateComponent라는 함수를 만든다. 이게 바로 렌더 effect의 본체다.instance.update = effect(updateComponent)- 즉시 1회 실행 → 최초 렌더 완료.
- 실행 중에 ref/reactive를 읽으면
track으로 렌더-상태 연결이 생긴다.
요점: 최초 렌더도 effect 실행의 결과다. mount가 effect를 등록하고 곧장 한 번 돌린다.
1. Counter의 라이프사이클(읽기 → 등록)
setup()에서const state = reactive({ count: 0 }),const double = computed(() => state.count * 2).render(ctx)가 실행되면ctx.count,ctx.double을 읽는다.ctx.count접근 → Proxyget→track(target=state, key='count')ctx.double접근 →computed.valuegetter → 필요 시 내부runner()로 계산, 그리고track(holder,'value')
결과적으로 렌더 effect는 두 dep에 구독한다:
(state,'count')와(computedHolder,'value').
2. 클릭 한 번에 무슨 일이 일어나나(쓰기 → 통지 → 재렌더)
버튼을 눌러 state.count++ 하는 순간을 프레임 단위로 분해해보자.
set트랩: Proxyset에서 이전 값과 새 값을 비교(Object.is). 값이 바뀌면trigger(state,'count').trigger는targetMap.get(state).get('count')로 구독 중인 effect 집합(dep) 을 얻는다.각 effect에 대해
e.scheduler ? e.scheduler() : queueJob(e)호출- 렌더 effect는 특별한 스케줄러가 없으므로 배치 큐(Set) 에 들어간다.
현재 tick의 동기 코드가 끝난 뒤, microtask에서
flushJobs()가 돌아 중복 제거된 effect들을 순서대로 실행한다.렌더 effect(
updateComponent) 재실행 → 새 VNode 트리를 만들고 이전 트리와patch로 비교 → 필요한 DOM만 갱신.
배치의 핵심: 클릭 여러 번을 같은 tick에서 연속으로 해도 렌더는 보통 한 번. 화면이 덜 흔들린다.
3. computed와의 상호작용
state.count가 바뀌면computed내부 effect의 스케줄러가 동작해 dirty = true 로만 바꾼다.- 렌더가 다시 실행되며
double.value를 읽을 때에만 실제 계산을 새로 돌린다(캐시 재활성화). - 따라서
computed는 "계산 결과를 구독하는 쪽"(여기선 렌더)에게trigger(holder,'value')로만 신호를 보낸다.
4. TodoList 흐름(입력/배열 조작)
input은ref(''):onInput에서ctx.input = e.target.value→ ref setter →triggerRefValue(ref)→ 렌더 effect 스케줄.
todos는reactive<Todo[]>([]):push/splice호출 시 Proxyset(또는length/인덱스 key)에서trigger발생.
렌더 갱신 시
patchChildren이 간단 순차 diff 로 리스트를 업데이트한다.
실무 팁: 큰 리스트에서 key 기반 diff(LIS) 최적화를 넣으면 재정렬 비용을 크게 줄일 수 있다. 현재 구현은 학습 목적의 순차 비교다.
5. 스케줄 타이밍(왜 microtask인가)
- 렌더 effect는 기본적으로
queueJob을 타고 microtask에서 flush된다. - 같은 tick 안에서 수십 번 상태를 바꿔도 렌더는 대부분 한 번으로 합쳐짐.
- 애니메이션/측정이 필요하면 watch에
flush:'pre'같은 스케줄을 줄 수 있다(렌더 전 실행).
6. 한 장으로 보는 시퀀스 다이어그램
User Click
└─ onClick → state.count++
└─ Proxy.set → trigger(state,'count')
└─ dep(effects).forEach(queueJob)
└─ microtask: flushJobs()
└─ effect(updateComponent)
├─ cleanup(old deps)
├─ render(ctx) // track 새로 수집
├─ patch(oldTree, newTree)
└─ DOM 업데이트요약
main.ts는 부팅 스위치다. mount에서 렌더 effect를 등록해 첫 화면을 그리고, 이후엔 상태 변화가 알아서 길을 잇는다.- Counter/TodoList는 ref/reactive/computed/watch가 각자 맡은 역할을 수행하며, 모든 길은 렌더 effect로 모인다.
- 이 구조를 이해하면, 실제 Vue에서도 문제가 터졌을 때 어디서 track이 끊겼는지, 어떤 trigger가 과하게 불탔는지를 감으로 잡아낼 수 있다.